/*!
\copyright  Copyright (c) 2020 - 2025 Qualcomm Technologies International, Ltd.
            All Rights Reserved.
            Qualcomm Technologies International, Ltd. Confidential and Proprietary.
\file
\brief      Kymera LE Voice chain handling
*/

#include "kymera_le_voice.h"
#include "kymera_state.h"
#include "kymera_config.h"
#include "kymera_dsp_clock.h"
#include "kymera_tones_prompts.h"
#include "kymera_aec.h"
#include "kymera_mic_if.h"
#include "kymera_output_if.h"
#include "kymera_data.h"
#include "kymera_leakthrough.h"
#include "kymera_le_common.h"
#include "kymera_le_mic_chain.h"
#include "kymera_le_audio_decoder.h"
#include "kymera_connect_utils.h"

#include "av.h"
#include <vmal.h>
#include <anc_state_manager.h>
#include <chain.h>
#include "multidevice.h"

#ifdef INCLUDE_HYBRID_CVC
#include "kymera_ml.h"
#endif

#ifdef INCLUDE_LE_AUDIO_UNICAST

/*! Code assertion that can be checked at run time. This will cause a panic. */
#define assert(x)   PanicFalse(x)

#define LC3_ENCODE_SCO_ISO_PACKET_LEN   40

#ifdef INCLUDE_CIS_MIRRORING
#define kymeraLeVoice_IsRelayEnabled() TRUE
#else
#define kymeraLeVoice_IsRelayEnabled() FALSE
#endif

/*! List of possible audio paths for LE voice call. */
typedef enum
{
    le_voice_mic_only_path,
    le_voice_speaker_only_path,
    le_voice_mic_speaker_path,
} le_voice_path_t;

static unsigned le_voice_sample_rate;

static bool use_split_stereo_chain;

/*! Utility macro to check if CIS supports both channels to be decoded */
#define kymeraLeVoice_IsStereoDecode(stream_type) (stream_type == KYMERA_LE_STREAM_STEREO_USE_BOTH && Multidevice_IsDeviceStereo())

static const output_registry_entry_t output_info_mono =
{
    .user = output_user_le_voice_mono,
    .connection = output_connection_mono,
};

static const output_registry_entry_t output_info_stereo =
{
    .user = output_user_le_voice_stereo,
    .connection = output_connection_stereo,
};

static const appKymeraLeVoiceChainTable *chain_config_map = NULL;
static kymera_chain_handle_t le_voice_chain = NULL;

static bool kymeraLeVoice_MicGetConnectionParameters(uint16 *mic_ids, Sink *mic_sinks, uint8 *num_of_mics, uint32 *sample_rate, Sink *aec_ref_sink);
static bool kymeraLeVoice_MicDisconnectIndication(const mic_change_info_t *info);
static mic_user_state_t kymeraLeVoice_GetUserState(void);
static bool kymeraLeVoice_GetAecRefUsage(void);
static uint32 kymeraLeVoice_GetMandatoryTtpDelay(void);

static const mic_callbacks_t kymeraLeVoice_Callbacks =
{
    .MicGetConnectionParameters = kymeraLeVoice_MicGetConnectionParameters,
    .MicDisconnectIndication = kymeraLeVoice_MicDisconnectIndication,
    .MicGetUserState = kymeraLeVoice_GetUserState,
    .MicGetAecRefUsage = kymeraLeVoice_GetAecRefUsage,
    .MicGetMandatoryTtpDelay = kymeraLeVoice_GetMandatoryTtpDelay,
};

static const mic_registry_per_user_t kymeraLeVoice_MicRegistry =
{
    .user = mic_user_le_voice,
    .callbacks = &kymeraLeVoice_Callbacks,
    .permanent.mandatory_mic_ids = NULL,
    .permanent.num_of_mandatory_mics = 0,
};

static void KymeraLeVoice_SetSampleRate(unsigned sample_rate)
{
    le_voice_sample_rate = sample_rate;
}

static unsigned KymeraLeVoice_GetSampleRate(void)
{
    return le_voice_sample_rate;
}

static uint16 kymeraLeVoice_PopulateConnectParams(uint16 *mic_ids, Sink *mic_sinks, uint8 num_mics, Sink *aec_ref_sink)
{
    kymera_chain_handle_t chain = le_voice_chain;
    PanicNull(mic_ids);
    PanicNull(mic_sinks);
    PanicNull(aec_ref_sink);

    aec_ref_sink[0] = ChainGetInput(chain, EPR_RATE_ADJUST_REF_IN);

    return Kymera_MapMicsToChainInputs(mic_ids, mic_sinks, num_mics, chain);
}

static bool kymeraLeVoice_MicDisconnectIndication(const mic_change_info_t *info)
{
    UNUSED(info);
    DEBUG_LOG_ERROR("kymeraLeVoice_MicDisconnectIndication: LE Voice shouldn't have to get disconnected");
    Panic();
    return TRUE;
}

static bool kymeraLeVoice_MicGetConnectionParameters(uint16 *mic_ids, Sink *mic_sinks, uint8 *num_of_mics, uint32 *sample_rate, Sink *aec_ref_sink)
{
    DEBUG_LOG("kymeraLeVoice_MicGetConnectionParameters");

    *sample_rate = KymeraLeVoice_GetSampleRate();
    *num_of_mics = kymeraLeVoice_PopulateConnectParams(mic_ids, mic_sinks, *num_of_mics, aec_ref_sink);

    return TRUE;
}

static mic_user_state_t kymeraLeVoice_GetUserState(void)
{
    return mic_user_state_non_interruptible;
}

static bool kymeraLeVoice_GetAecRefUsage(void)
{
    return TRUE;
}

static uint32 kymeraLeVoice_GetMandatoryTtpDelay(void)
{
    uint32 kymera_mandatory_le_voice_ttp_Delay = 0;

    if (appKymeraGetState () == KYMERA_STATE_LE_VOICE_ACTIVE)
    {
        kymera_mandatory_le_voice_ttp_Delay = LE_AUDIO_LC3_MANDATORY_TTP_DELAY_IN_US;
    }

    DEBUG_LOG("kymeraLeVoice_GetMandatoryTtpDelay %d us",kymera_mandatory_le_voice_ttp_Delay);

    return kymera_mandatory_le_voice_ttp_Delay;
}

void Kymera_SetLeVoiceChainTable(const appKymeraLeVoiceChainTable *chain_table)
{
    chain_config_map = chain_table;
}

static kymera_chain_handle_t KymeraLeVoice_CreateChain(const chain_config_t *chain_config)
{
    DEBUG_LOG("KymeraLeVoice_CreateChain");

    /* Create chain and return handle */
    PanicNotNull(le_voice_chain);
    le_voice_chain = ChainCreate(chain_config);

#ifdef INCLUDE_HYBRID_CVC
    Kymera_MlEngineCreate();
#endif

    /* Configure DSP power mode appropriately for LE voice chain */
    appKymeraConfigureDspPowerMode();

    return le_voice_chain;
}

static void kymeraLeVoice_ConfigureLc3Encoder(kymera_chain_handle_t chain_handle, const le_microphone_config_t *le_mic_params)
{
    Operator op = ChainGetOperatorByRole(chain_handle, OPR_LC3_ENCODE_SCO_ISO);
    OperatorsLc3EncoderScoIsoSetPacketLength(op, le_mic_params->frame_length);
    OperatorsStandardSetSampleRate(op, le_mic_params->sample_rate);
    OperatorsLc3EncoderScoIsoSetFrameDuration(op,le_mic_params->frame_duration);

    if (le_mic_params->codec_version == 1)
    {
        OperatorsLc3EncoderScoIsoSetErrorResilience(op, lc3_epc_low);
    }
}

/*! \brief configure the LC3 encoder in Voice chain */
static void kymeraLeVoice_ConfigureChain(const KYMERA_INTERNAL_LE_VOICE_START_T *start_params, le_voice_path_t voice_path)
{
    kymera_chain_handle_t chain = le_voice_chain;
    Operator op = ChainGetOperatorByRole(chain, OPR_RATE_ADJUST);

    PanicNull((void *)chain);

    DEBUG_LOG("kymeraLeVoice_ConfigureChain voice path : enum:le_voice_path_t:%d", voice_path);

    if (voice_path != le_voice_speaker_only_path)
    {
        /* Configure LC3 ENCODER */
        kymeraLeVoice_ConfigureLc3Encoder(chain, &start_params->microphone_params);
        OperatorsStandardSetSampleRate(op, start_params->microphone_params.sample_rate);
    }

    /* Configure CVC SEND
        full_processing = 0x2
        pass_through = 0x3
    */

    Kymera_SetVoiceUcids(chain);
#ifdef INCLUDE_HYBRID_CVC
    Operator cvc_send = PanicZero(ChainGetOperatorByRole(le_voice_chain, OPR_CVC_SEND));
    kymera_ml_config_t ml_config = {
        .use_case_id = 1,
        .access_method = model_direct_access,
        .batch_reset_count = 0,
        .model_file = "cvc_noise_sup.keai"
    };
    Kymera_MlActivate(cvc_send, &ml_config);
    MessageOperatorTask(cvc_send, KymeraGetTask());
#endif
}

static void kymeraLeVoice_ConfigureEndpoints(Sink voice_sink)
{
    kymera_chain_handle_t chain = le_voice_chain;
    PanicNull((void *)chain);

    Operator op = ChainGetOperatorByRole(chain, OPR_RATE_ADJUST);
    DEBUG_LOG("kymeraLeVoice_ConfigureEndpoints op_id=%d sink=%d", op, voice_sink);
    SinkConfigure(voice_sink, STREAM_RM_USE_RATE_ADJUST_OPERATOR, op);
}

static const chain_config_t *kymeraLeVoice_FindChainFromConfigTable(const appKymeraLeVoiceChainTable *chain_config, uint16 sample_rate, le_voice_path_t voice_path)
{
    unsigned index;
    uint8 mic = (voice_path == le_voice_speaker_only_path) ? 0: Microphones_GetNumberOfMicrophonesForVoice();

    assert(chain_config);

    DEBUG_LOG("kymeraLeVoice_FindChainFromConfigTable chain_table %p chain_length %u, voice_path %d",
              chain_config->chain_table, chain_config->table_length, voice_path);

    for (index = 0; index < chain_config->table_length; index++)
    {
        /* Choose matching chain from voice chain table, however in the absence of microphone path we use non CVC
           lc3 decoder chain to render speaker path disregarding sample rate matching */
        if(chain_config->chain_table[index].is_stereo_config == use_split_stereo_chain && 
            chain_config->chain_table[index].mic_count == mic && (mic == 0 || chain_config->chain_table[index].sample_rate == sample_rate))
        {
            DEBUG_LOG("kymeraLeVoice_FindChainFromConfigTable : Voice chain from address %p selected", chain_config->chain_table[index].chain_config);
            return chain_config->chain_table[index].chain_config;
        }
    }

    DEBUG_LOG("kymeraLeVoice_FindChainFromConfigTable: No compatible chain configuration found!");
    Panic();

    return NULL;
}

static le_voice_path_t kymeraLeVoice_GetVoicePath(const KYMERA_INTERNAL_LE_VOICE_START_T * start_params)
{
    le_voice_path_t voice_path;

    if (start_params->microphone_present && start_params->speaker_present)
    {
        voice_path = le_voice_mic_speaker_path;
    }
    else if (start_params->microphone_present)
    {
        voice_path = le_voice_mic_only_path;
    }
    else
    {
        voice_path = le_voice_speaker_only_path;
    }

    DEBUG_LOG("kymeraLeVoice_GetVoicePath: enum:le_voice_path_t:%d",voice_path);

    return voice_path;
}

static void kymeraLeVoice_SetupToAirChain(const KYMERA_INTERNAL_LE_VOICE_START_T *start_params, le_voice_path_t voice_path)
{
    DEBUG_LOG("kymeraLeVoice_SetupToAirChain: codec_type %d, codec version %d, frame_duration %d, sample_rate %d, frame_length %d, presentation_delay %d",
             start_params->microphone_params.codec_type, start_params->microphone_params.codec_version, start_params->microphone_params.frame_duration, start_params->microphone_params.sample_rate,
             start_params->microphone_params.frame_length, start_params->microphone_params.presentation_delay);

    if (voice_path == le_voice_mic_only_path)
    {
        Kymera_CreateLeMicChain(&start_params->microphone_params, FALSE);
    }
}

static void kymeraLeVoice_SetupFromAirChain(const KYMERA_INTERNAL_LE_VOICE_START_T *start_params, le_voice_path_t voice_path)
{
    const chain_config_t *chain_config = NULL;
    kymeraTaskData *theKymera = KymeraGetTaskData();

    if (voice_path == le_voice_mic_only_path)
    {
        return;
    }

    use_split_stereo_chain = (start_params->speaker_params.iso_handle_right != LE_AUDIO_INVALID_ISO_HANDLE && !kymeraLeVoice_IsRelayEnabled()) || kymeraLeVoice_IsStereoDecode(start_params->speaker_params.stream_type);
    chain_config = kymeraLeVoice_FindChainFromConfigTable(chain_config_map, start_params->speaker_params.sample_rate , voice_path);

    DEBUG_LOG("kymeraLeVoice_SetupFromAirChain, single audio location/mono/stereo ISO 0x%x, right ISO 0x%x, sink 0x%x, vol %d, enum:appKymeraState:%d",
              start_params->speaker_params.iso_handle, start_params->speaker_params.iso_handle_right, start_params->microphone_params.source_iso_handle, start_params->volume_in_db, appKymeraGetState());

    DEBUG_LOG("codec_type %d, codec version %d, frame_duration %d, sample_rate %d, frame_length %d, presentation_delay %d",
             start_params->speaker_params.codec_type, start_params->speaker_params.codec_version, start_params->speaker_params.frame_duration, start_params->speaker_params.sample_rate,
             start_params->speaker_params.frame_length, start_params->speaker_params.presentation_delay);

    /* Create appropriate LE Voice chain */
    PanicNull(KymeraLeVoice_CreateChain(chain_config));

    /* Create and configure LE Audio decoder chain */
    KymeraLeAudioDecoder_CreateChain(&start_params->speaker_params);

    KymeraLeAudioDecoder_ConfigureChain(&start_params->speaker_params);

    if (voice_path != le_voice_speaker_only_path)
    {
        /* Connect to Mic interface */
        if (!Kymera_MicConnect(mic_user_le_voice))
        {
            DEBUG_LOG_ERROR("kymeraLeVoice_SetupFromAirChain: Mic connection was not successful. LE Voice should always be prepared.");
            Panic();
        }
    }

    /* Configure LC3 Encoder */
    kymeraLeVoice_ConfigureChain(start_params, voice_path);

    /* Select output user depending on CIS configuration */
    output_users_t output_user_le_voice = use_split_stereo_chain ? output_user_le_voice_stereo : output_user_le_voice_mono;

    /* Create an appropriate Output chain */
    kymera_output_chain_config output_config;
    KymeraOutput_SetDefaultOutputChainConfig(&output_config, start_params->speaker_params.sample_rate, KICK_PERIOD_VOICE, 0);

    output_config.chain_type = use_split_stereo_chain? output_chain_stereo_le : output_chain_mono_le;
     output_config.chain_include_aec = (voice_path == le_voice_speaker_only_path) ? FALSE : TRUE;
    PanicFalse(Kymera_OutputPrepare(output_user_le_voice, &output_config));

    /* Get sources and sinks for chain endpoints */
    Source to_air_ep = ChainGetOutput(le_voice_chain, EPR_ISO_TO_AIR_LEFT);

    /* Connect ISO to chain ISO endpoints */
    Sink to_air_iso_sink = StreamIsoSink(start_params->microphone_params.source_iso_handle);

    /* Connect LE Audio decoder chain output to LE Voice input chain */
    Source decoder_chain_output = KymeraLeAudioDecoder_GetOutput(EPR_SOURCE_DECODED_PCM);
    PanicFalse(ChainConnectInput(le_voice_chain, decoder_chain_output, EPR_SOURCE_DECODED_PCM));

    if ((start_params->speaker_params.iso_handle_right != LE_AUDIO_INVALID_ISO_HANDLE && 
        start_params->speaker_params.stream_type == KYMERA_LE_STREAM_DUAL_MONO) ||
        kymeraLeVoice_IsStereoDecode(start_params->speaker_params.stream_type))
    {
        /* Connect LE Audio decoder chain output right to LE Voice input chain */
        Source decoder_chain_output_right = KymeraLeAudioDecoder_GetOutput(EPR_SOURCE_DECODED_PCM_RIGHT);

        PanicFalse(ChainConnectInput(le_voice_chain, decoder_chain_output_right, EPR_SOURCE_DECODED_PCM_RIGHT));
    }

    if (voice_path != le_voice_speaker_only_path)
    {
        kymeraLeVoice_ConfigureEndpoints(to_air_iso_sink);
        StreamConnect(to_air_ep, to_air_iso_sink);
    }

    if (theKymera->chain_config_callbacks && theKymera->chain_config_callbacks->ConfigureLeVoiceChain)
    {
        kymera_le_voice_config_params_t params = {0};

        if (start_params->speaker_present)
        {
            params.speaker_path_present = start_params->speaker_present;
            params.to_air.codec_type = start_params->speaker_params.codec_type;
            params.to_air.sample_rate = start_params->speaker_params.sample_rate;
            params.to_air.frame_length = start_params->speaker_params.frame_length;
            params.to_air.frame_duration_us = start_params->speaker_params.frame_duration;
            params.to_air.presentation_delay_us = start_params->speaker_params.presentation_delay;
            params.to_air.audio_channel_mask = start_params->speaker_params.audio_channel_mask;
        }
        
        if (start_params->microphone_present)
        {
            params.microphone_path_present = start_params->microphone_present;
            params.from_air.codec_type = start_params->microphone_params.codec_type;
            params.from_air.sample_rate = start_params->microphone_params.sample_rate;
            params.from_air.frame_length = start_params->microphone_params.frame_length;
            params.from_air.frame_duration_us = start_params->microphone_params.frame_duration;
            params.from_air.presentation_delay_us = start_params->microphone_params.presentation_delay;
            params.from_air.audio_channel_mask = start_params->microphone_params.audio_channel_mask;
        }

        theKymera->chain_config_callbacks->ConfigureLeVoiceChain(le_voice_chain, &params);
    }

    /* Connect chain */
    ChainConnect(le_voice_chain);
}

static void kymeraLeVoice_StartFromAirChain(const KYMERA_INTERNAL_LE_VOICE_START_T *start_params, le_voice_path_t voice_path)
{
    kymera_chain_handle_t chain = NULL;
    /* Select output user depending on CIS configuration */
    output_users_t output_user_le_voice = use_split_stereo_chain ? output_user_le_voice_stereo : output_user_le_voice_mono;
    /* Connect to the Output chain */
    output_source_t sources;

    if (voice_path == le_voice_mic_only_path)
    {
        return;
    }

    DEBUG_LOG("kymeraLeVoice_StartFromAirChain");

    chain = PanicNull(le_voice_chain);

    if (use_split_stereo_chain)
    {
        sources.stereo.left = ChainGetOutput(chain, EPR_SCO_SPEAKER);
        sources.stereo.right = ChainGetOutput(chain, EPR_SCO_SPEAKER_RIGHT);
    }
    else
    {
        sources.mono = ChainGetOutput(chain, EPR_SCO_SPEAKER);
    }

    KymeraLeAudioDecoder_StartChain();

    PanicFalse(Kymera_OutputConnect(output_user_le_voice, &sources));
    KymeraOutput_ChainStart();

    /* The chain can fail to start if the ISO source disconnects whilst kymera
       is queuing the ISO start request or starting the chain. If the attempt
       fails, ChainStartAttempt will stop (but not destroy) any operators it
       started in the chain. */
    if (ChainStartAttempt(chain))
    {
        KymeraLeVoice_HandleInternalSetVolume(start_params->volume_in_db);
        KymeraLeAudioVoice_SetMicMuteState(start_params->microphone_params.mic_mute_state);
#ifndef FORCE_CVC_PASSTHROUGH
        kymeraTaskData *theKymera = KymeraGetTaskData();
        if(theKymera->enable_cvc_passthrough)
#endif
        {
            Kymera_SetCvcPassthroughMode(KYMERA_CVC_RECEIVE_PASSTHROUGH | KYMERA_CVC_SEND_PASSTHROUGH, 0);
        }

        if(AecLeakthrough_IsLeakthroughEnabled())
        {
           Kymera_LeakthroughSetAecUseCase(aec_usecase_enable_leakthrough);
        }
    }
    else
    {
        DEBUG_LOG("KymeraLeVoice_HandleInternalStart, could not start chain");
        /* Stop/destroy the chain, returning state to KYMERA_STATE_IDLE.
           This needs to be done here, since between the failed attempt to start
           and the subsequent stop (when appKymeraScoStop() is called), a tone
           may need to be played - it would not be possible to play a tone in a
           stopped SCO chain. The state needs to be KYMERA_STATE_LE_VOICE_ACTIVE for
           KymeraLeVoice_HandleInternalStop() to stop/destroy the chain. */
        KymeraLeVoice_HandleInternalStop();
    }
}

static void kymeraLeVoice_StartToAirChain(le_voice_path_t voice_path)
{
    if (voice_path == le_voice_mic_only_path)
    {
        DEBUG_LOG("kymeraLeVoice_StartToAirChain");
        Kymera_StartLeMicChain();
        /* Ensure to unmute the mice */
        KymeraLeAudioVoice_SetMicMuteState(FALSE);
    }
}

void KymeraLeVoice_HandleInternalStart(const KYMERA_INTERNAL_LE_VOICE_START_T * start_params)
{
    le_voice_path_t voice_path = kymeraLeVoice_GetVoicePath(start_params);

    /* If there is a tone still playing at this point,
     * it must be an interruptible tone, so cut it off */
    appKymeraTonePromptStop();

    if(appKymeraGetState() == KYMERA_STATE_STANDALONE_LEAKTHROUGH)
    {
        Kymera_LeakthroughStopChainIfRunning();
        appKymeraSetState(KYMERA_STATE_IDLE);
    }

    /* Can't start voice chain if we're not idle */
    PanicFalse(appKymeraGetState() == KYMERA_STATE_IDLE || appKymeraGetState() == KYMERA_STATE_ADAPTIVE_ANC_STARTED);

    /* LE Voice chain must be destroyed if we get here */
    PanicNotNull(le_voice_chain);

    /* Move to LE Voice active state now, what ever happens we end up in this state
      (even if it's temporary) */
    appKymeraSetState(KYMERA_STATE_LE_VOICE_ACTIVE);

    /* Momentarily boosting DSP clock for faster chain creation and setup */
    appKymeraBoostDspClockToMax();

    KymeraLeVoice_SetSampleRate(start_params->speaker_params.sample_rate);

#ifdef INCLUDE_SWB_LC3
    PanicFalse(Kymera_AecSetTtpDelayBeforeConnection(kymeraLeVoice_GetMandatoryTtpDelay()));
#endif

    kymeraLeVoice_SetupToAirChain(start_params, voice_path);

    kymeraLeVoice_SetupFromAirChain(start_params, voice_path);

    kymeraLeVoice_StartFromAirChain(start_params, voice_path);

    kymeraLeVoice_StartToAirChain(voice_path);
}

void KymeraLeVoice_HandleInternalStop(void)
{
    DEBUG_LOG("KymeraLeVoice_HandleInternalStop, state %u", appKymeraGetState());

    if (appKymeraGetState() != KYMERA_STATE_LE_VOICE_ACTIVE)
    {
        if (le_voice_chain == NULL)
        {
            /* Attempting to stop a LE Voice chain when not ACTIVE. This happens
               when the user calls Kymera_LeVoiceStop() following a failed
               attempt to start the LE Voice chain - see ChainStartAttempt() in
               KymeraLeVoice_HandleInternalStart().
               In this case, there is nothing to do, since the failed start
               attempt cleans up by calling this function in state
               KYMERA_STATE_LE_VOICE_ACTIVE */
            DEBUG_LOG("KymeraLeVoice_HandleInternalStop, not stopping - already idle");
            return;
        }
        else
        {
            Panic();
        }
    }

    if (le_voice_chain != NULL)
    {
        Source iso_ep_src = ChainGetOutput(le_voice_chain, EPR_ISO_TO_AIR_LEFT);
        Sink iso_ep_snk = ChainGetInput(le_voice_chain, EPR_ISO_FROM_AIR_LEFT);
        Sink iso_ep_snk_right = ChainGetInput(le_voice_chain, EPR_ISO_FROM_AIR_RIGHT);

        /* Disable AEC_REF sidetone path */
        Kymera_LeakthroughEnableSidetonePath(FALSE);

        /* A tone still playing at this point must be interruptable */
        appKymeraTonePromptStop();

        /* Stop chains */
        ChainStop(le_voice_chain);
        KymeraLeAudioDecoder_StopChain();

        /* Disconnect SCO from chain SCO endpoints */
        StreamDisconnect(iso_ep_src, NULL);
        StreamDisconnect(NULL, iso_ep_snk);
        StreamDisconnect(NULL, iso_ep_snk_right);

        Kymera_MicDisconnect(mic_user_le_voice);

        output_users_t output_user_le_voice = use_split_stereo_chain ? output_user_le_voice_stereo : output_user_le_voice_mono;
        Kymera_OutputDisconnect(output_user_le_voice);

#ifdef INCLUDE_HYBRID_CVC
        Operator cvc_send = PanicZero(ChainGetOperatorByRole(le_voice_chain, OPR_CVC_SEND));
        Kymera_MlDeactivate(cvc_send);
#endif

        /* Destroy chains */
        ChainDestroy(le_voice_chain);
        le_voice_chain = NULL;

#ifdef INCLUDE_HYBRID_CVC
        Kymera_MlEngineDestroy();
#endif
    }
    else
    {
        Kymera_LeMicDisconnectOutputChain();
        Kymera_StopLeMicChain();
    }

    /* Update state variables */
    appKymeraSetState(KYMERA_STATE_IDLE);

    Kymera_LeakthroughResumeChainIfSuspended();
}

void KymeraLeVoice_HandleInternalSetVolume(int16 volume_in_db)
{
    DEBUG_LOG("KymeraLeVoice_HandleInternalSetVolume, vol %d", volume_in_db);

    switch (KymeraGetTaskData()->state)
    {
    case KYMERA_STATE_LE_VOICE_ACTIVE:
        KymeraOutput_SetMainVolume(volume_in_db);
        break;

    default:
        break;
    }
}

void kymeraLeVoice_HandleInternalMicMute(bool mute)
{
    DEBUG_LOG("kymeraLeVoice_HandleInternalMicMute, mute %u", mute);

    switch (KymeraGetTaskData()->state)
    {
    case KYMERA_STATE_LE_VOICE_ACTIVE:
        KymeraLeAudioVoice_SetMicMuteState(mute);
        break;

    default:
        break;
    }
}

kymera_chain_handle_t Kymera_LeVoiceGetCvcChain(void)
{
    return le_voice_chain;
}

bool Kymera_IsLeVoiceSplitStereoChain(void)
{
    return use_split_stereo_chain;
}

void KymeraLeVoice_Init(void)
{
    Kymera_OutputRegister(&output_info_mono);
    Kymera_OutputRegister(&output_info_stereo);
    Kymera_MicRegisterUser(&kymeraLeVoice_MicRegistry);
}

#endif
